feat(fx): real applied rate for FCY conversions (issue #253)#254
Conversation
…sue #253) When a EUR↔FCY conversion carries the broker's real applied EUR amount, value it at that effective rate instead of the official ECB mid-rate, so the broker's hidden FX spread is captured as a deductible cost (Art. 35.1 LIRPF). ECB stays the default and the fallback, and remains the valuation for FCY that arrives with no euro price (stock proceeds, dividends, interest). - types: add optional Trade.realEurAmount (real EUR FX principal, fee excluded; NOT fxRateToBase, which stays banned). - engine (fx-fifo.ts): one shared effectiveRate() helper used by BOTH addLot and consumeLots, so acquire and dispose legs are always valued on the same basis — the symmetry invariant that prevents a phantom gain. Absent/zero/ non-finite realEurAmount falls back to ECB (byte-identical to prior behavior). Commission still applied on top (no fee double-count). - parser (lightyear.ts): harvest the conversion pair's EUR-leg |Net Amt.| and stamp it on the non-EUR CASH trade. Two EUR legs sharing a timestamp are ambiguous → drop to ECB rather than mis-attach a principal. Pinned by tests/engine/fx-fifo.test.ts (spread capture, symmetry, no double- count, byte-identical fallback, zero-amount self-guard), tests/parsers/ lightyear-fx-realrate.test.ts (parser harvest, reconciliation, ambiguity drop), and tests/integration/fx-real-rate-e2e.test.ts (end-to-end).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #254 +/- ##
==========================================
+ Coverage 95.30% 95.34% +0.04%
==========================================
Files 54 54
Lines 5192 5219 +27
Branches 1739 1755 +16
==========================================
+ Hits 4948 4976 +28
Misses 210 210
+ Partials 34 33 -1
🚀 New features to boost your workflow:
|
|
Warning Review limit reached
More reviews will be available in 52 minutes and 17 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds an optional ChangesReal EUR Amount FX Effective-Rate Valuation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- effectiveRate: reject negative realEurAmount (a real principal is positive) instead of absorbing it via abs(); 0/NaN/non-finite still fall back to ECB. - lightyear: use the EUR-leg Net Amt. only as the real principal; drop the fee-inclusive Gross fallback that would double-count the fee when Net is empty (omit the field → ECB instead).
Summary
Closes #253. When a broker reports the real EUR amount it actually applied to a EUR↔FCY conversion, DeclaRenta now values that conversion at the broker's effective rate (
realEurAmount / |quantity|) instead of the official ECB mid-rate — so the broker's hidden FX spread is captured as a deductible cost (Art. 35.1.a "importe real" + 35.1.b "gastos inherentes"). ECB remains the default, the fallback, and the valuation for FCY that arrives with no euro price (stock-sale proceeds, dividends, interest).This carves out the narrow "real conversion" case the maintainer already endorsed in #242 — it does not overturn the "always ECB, never
fxRateToBase" rule:realEurAmountis a real cash amount from the statement, not a broker-derived rate.Design
Trade.realEurAmount?— the real EUR FX principal (spread embedded, the separately-itemized fee EXCLUDED). The existingcommissionis still applied on top, sorealEurAmount + commission= full real cost (no fee double-count).effectiveRate()helper infx-fifo.ts, used by bothaddLot(acquire) andconsumeLots(dispose). This is the load-bearing safety invariant: both legs of a tracked round-trip are always valued on the same basis — never a half-real/half-ECB mix, which is the only thing that could fabricate a phantom gain. Both legs real → realized FX gain =realEurReceived − realEurPaidexactly.realEurAmount→effectiveRatereturnsecbRate→ prior behavior unchanged. Pinned by a regression test.|Net Amt.|); every other broker falls back to ECB until its parser sets the field. Skipped under monodivisa (FX engine off).report.ts/CLI/web/profile changes: the field rides on theTrade, read inextractFxEvents, andmerge.tscarries it automatically.Robustness (from code review)
effectiveRateself-guards finite & strictly-positive (a stray0/negative/NaN can never zero a conversion → ECB).extractFxEventsparsesrealEurAmountdefensively (garbage → ECB, no crash).Testing
npm run typecheck,npm run typecheck:tests,npm run lint— clean.tests/engine/fx-fifo.test.ts(spread capture, symmetry, commission-on-top, byte-identical fallback, zero-amount self-guard),tests/parsers/lightyear-fx-realrate.test.ts(harvest,realEur + |fee| == |EUR Gross|, ambiguous-timestamp drop),tests/integration/fx-real-rate-e2e.test.ts(end-to-end throughgenerateTaxReport).Summary by CodeRabbit
Release Notes
New Features
Improvements